import {
  ASTUtils,
  RuleFixes,
  isNotNullOrUndefined,
  Selectors,
  toPattern,
} from '@angular-eslint/utils';
import type { TSESTree } from '@typescript-eslint/utils';
import { createESLintRule } from '../utils/create-eslint-rule';

export type Options = [];
export type MessageIds =
  | 'noEmptyLifecycleMethod'
  | 'suggestRemoveLifecycleMethod';
export const RULE_NAME = 'no-empty-lifecycle-method';

export default createESLintRule<Options, MessageIds>({
  name: RULE_NAME,
  meta: {
    type: 'suggestion',
    docs: {
      description: 'Disallows declaring empty lifecycle methods',
      recommended: 'recommended',
    },
    hasSuggestions: true,
    schema: [],
    messages: {
      noEmptyLifecycleMethod: 'Lifecycle methods should not be empty',
      suggestRemoveLifecycleMethod: 'Remove lifecycle method',
    },
  },
  defaultOptions: [],
  create(context) {
    const sourceCode = context.sourceCode;

    const angularDecoratorsPattern = toPattern([
      'Component',
      'Directive',
      'Injectable',
      'NgModule',
      'Pipe',
    ]);

    const angularLifecycleMethodsPattern = toPattern([
      ...ASTUtils.ANGULAR_LIFECYCLE_METHODS,
    ]);

    return {
      [`${Selectors.decoratorDefinition(
        angularDecoratorsPattern,
      )} > ClassBody > ${Selectors.methodDefinition(
        angularLifecycleMethodsPattern,
      )}[value.body.body.length=0]`](
        node: TSESTree.MethodDefinition & {
          parent: TSESTree.ClassBody & { parent: TSESTree.ClassDeclaration };
        },
      ) {
        context.report({
          node,
          messageId: 'noEmptyLifecycleMethod',
          suggest: [
            {
              messageId: 'suggestRemoveLifecycleMethod',
              fix: (fixer) => {
                const importDeclarations =
                  ASTUtils.getImportDeclarations(node, '@angular/core') ?? [];
                const interfaceName = ASTUtils.getRawText(node).replace(
                  /^ng+/,
                  '',
                );
                const text = sourceCode.getText();
                const totalInterfaceOccurrences = getTotalInterfaceOccurrences(
                  text,
                  interfaceName,
                );
                const totalInterfaceOccurrencesSafeForRemoval = 2;

                return [
                  fixer.remove(node),
                  RuleFixes.getImplementsRemoveFix(
                    sourceCode,
                    node.parent.parent,
                    interfaceName,
                    fixer,
                  ),
                  totalInterfaceOccurrences <=
                  totalInterfaceOccurrencesSafeForRemoval
                    ? RuleFixes.getImportRemoveFix(
                        sourceCode,
                        importDeclarations,
                        interfaceName,
                        fixer,
                      )
                    : null,
                ].filter(isNotNullOrUndefined);
              },
            },
          ],
        });
      },
    };
  },
});

function stripSpecialCharacters(text: string) {
  return text.replace(/[\W]/g, '');
}

function getTotalInterfaceOccurrences(text: string, interfaceName: string) {
  return text
    .split(' ')
    .filter((item) => stripSpecialCharacters(item) === interfaceName).length;
}

export const RULE_DOCS_EXTENSION = {
  rationale:
    'Empty lifecycle methods add noise to the codebase without providing any value. When a developer sees ngOnInit(), ngOnDestroy(), or other lifecycle hooks, they expect to find important initialization, cleanup, or other lifecycle-related logic. Finding an empty method wastes time during code review and maintenance. Empty lifecycle methods often remain after code has been refactored or removed, serving as dead code that clutters the component. They also require unnecessary imports of lifecycle interfaces (like OnInit, OnDestroy). Removing empty lifecycle methods makes components cleaner, easier to understand, and reduces the file size.',
};
